#!/usr/bin/ruby -Eutf-8
# encoding: utf-8
#
# Find dependencies between ruby packages
#
# Must run inside a openwrt with all *ruby* packages installed
#

require "rbconfig"

RUBY_SIMPLE_VERSION = RUBY_VERSION.split(".")[0..1].join(".")
failed = false

puts "Loading all installed gems (unstable after external gems are instaled/update)"
require 'rubygems'
Gem::Specification.collect{ |g| g.name.downcase }.uniq.each {|g| gem g }

puts "Looking for installed ruby packages..."
packages=`opkg list-installed '*ruby*' | cut -d' ' -f 1`.split("\n")

puts "Looking for packages files..."
package_files=Hash.new { |h,k| h[k]=[] }
packages.each do
	|pkg|
	files=`opkg files "#{pkg}" | sed -e 1d`.split("\n")
	package_files[pkg]=files if files
end
# Fake enc/utf_16 to dummy enc:
package_files["ruby-enc"]+=[RbConfig::CONFIG["rubylibdir"] + "/enc/utf_16.rb" ]

require_regex=/^ *require ["']([^"']+)["'].*/
require_regex_ignore=/^ *require ([a-zA-Z\$]|["']\$|.*\/$|.*#.*|.*\.$)/
require_ignore=%w{
	bundler
	capistrano/version
	coverage/helpers
	dbm
	ffi
	fiber
	foo
	gettext/mo
	gettext/po_parser
	graphviz
	iconv
	java
	jruby
	json/pure
	minitest/proveit
	open3/jruby_windows
	prism/prism
	profile
	racc/cparse-jruby.jar
	repl_type_completor
	rubygems/defaults/operating_system
	rubygems/net/http
	rubygems/timeout
	sorted_set
	stackprof
	thread
	tracer
	uconv
	webrick
	webrick/https
	win32api
	win32ole
	win32/resolv
	win32/sspi
	xml/encoding-ja
	xmlencoding-ja
	xml/parser
	xmlparser
	xmlscan/scanner
}

matched_ignored={}

builtin_enc=[
	Encoding.find("ASCII-8BIT"),
	Encoding.find("UTF-8"),
	Encoding.find("UTF-7"),
	Encoding.find("US-ASCII"),
]

puts "Looking for requires in files..."
files_requires=Hash.new { |h,k| h[k]=[] }
packages.each do
        |pkg|
	package_files[pkg].each do
		|file|
		next if not File.file?(file)

		if not file =~ /.rb$/
			if File.executable?(file)
				magic=`head -c50 '#{file}' | head -1`
				begin
					if not magic =~ /ruby/
						next
					end
				rescue
					next
				end
			else
				next
			end
		end
		#puts "Checking #{file}..."
		File.open(file, "r") do
			|f|
			lineno=0
			while line=f.gets() do
				lineno+=1; encs=[]; requires=[]; need_encdb=false

				line=line.chomp.gsub!(/^[[:blank:]]*/,"")

				case line
				when /^#.*coding *:/
					if lineno <= 2
						enc=line.sub(/.*coding *: */,"").sub(/ .*/,"")
						encs << Encoding.find(enc)
					end
				end
				line.gsub!(/#.*/,"")
				case line
				when "__END__"
					break
				when /^require /
					#puts "#{file}:#{line}"
					if require_regex_ignore =~ line
						puts "Ignoring #{line} at #{file}:#{lineno} (REGEX)..."
						next
					end
					if not require_regex =~ line
						puts "Unknown require: '#{line}' at file #{file}:#{lineno} and it did not match #{require_regex_ignore}"
						failed=true
					end
					require=line.gsub(require_regex,"\\1")
					require.gsub!(/\.(so|rb)$/,"")

					if require_ignore.include?(require)
						puts "Ignoring #{line} at #{file}:#{lineno} (STR)..."
                                                matched_ignored[require]=1
						next
					end

					files_requires[file] += [require]

				when /Encoding::/
					encs=line.scan(/Encoding::[[:alnum:]_]+/).collect {|enc| eval(enc) }.select {|enc| enc.kind_of? Encoding }
					need_encdb=true
				end

				next if encs.empty?
				required_encs = (encs - builtin_enc).collect {|enc| "enc/#{enc.name.downcase.gsub("-","_")}" }
				required_encs << "enc/encdb" if need_encdb

				files_requires[file] += required_encs
			end
		end
	end
end
exit(1) if failed

missed_ignored = (require_ignore - matched_ignored.keys).sort.join(",")
if not missed_ignored.empty?
    puts "These 'require_ignore' didn't match anything: ",(require_ignore - matched_ignored.keys).sort.join(","),""
end

# From ruby source: grep -E 'rb_require' -R . | grep -E '\.c:.*rb_require.*'
# Add dependencies of ruby files from ruby lib.so
package_files.each do |(pkg,files)| files.each do |file|
	case file
	when /\/nkf\.so$/    ; files_requires[file]=files_requires[file] + ["enc/encdb"]
	when /\/objspace\.so$/; files_requires[file]=files_requires[file] + ["tempfile"] 	# dump_output from ext/objspace/objspace_dump.c
	when /\/openssl\.so$/; files_requires[file]=files_requires[file] + ["digest"] 		# Init_ossl_digest from ext/openssl/ossl_digest.c
	end
end; end

puts "Grouping package requirements per package"
package_requires_files = Hash.new{|h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } }
package_files.each do |(pkg,files)|
	package_requires_files[pkg]
	files.each do |file|
		files_requires[file].each do |requires|
			package_requires_files[pkg][requires] << file
		end
	end
end

# For optional require or for breaking cycle dependencies
weak_dependency=Hash.new { |h,k| h[k]=[] }
weak_dependency.merge!({
	"ruby-irb"      =>%w{ruby-rdoc ruby-readline ruby-debug}, # irb/cmd/help.rb irb/cmd/debug.rb,3.2/irb/cmd/debug.rb 
	"ruby-gems"     =>%w{ruby-bundler ruby-rdoc},             # rubygems.rb rubygems/server.rb rdoc/rubygems_hook
	"ruby-racc"     =>%w{ruby-gems},			  # /usr/bin/racc*
	"ruby-rake"     =>%w{ruby-gems ruby-debug},               # /usr/bin/rake gems/3.3/gems/rake-13.1.0/lib/rake/application.rb
	"ruby-rdoc"     =>%w{ruby-readline},			  # rdoc/ri/driver.rb
	"ruby-testunit" =>%w{ruby-io-console},			  # gems/test-unit-3.1.5/lib/test/unit/ui/console/testrunner.rb
	"ruby-net-http" =>%w{ruby-open-uri}			  # net/http/status.rb
})

puts "Looking for package dependencies..."
package_provides = {}
package_dependencies = Hash.new { |h,k| h[k]=[] }
package_requires_files.each do
	|(pkg,requires_files)|

	requires_files.each do
		|(require,files)|
		if package_provides.include?(require)
			found = package_provides[require]
		else
			found = package_files.detect {|(pkg,files)| files.detect {|file| $:.detect {|path| "#{path}/#{require}" == file.gsub(/\.(so|rb)$/,"") } } }
			if not found
				$stderr.puts "#{pkg}: Nothing provides #{require} for #{files.collect {|file| file.sub("/usr/lib/ruby/","") }.join(",")}"
				failed = true
				next
			end
			found = found.first
			package_provides[require] = found
		end
		if weak_dependency[pkg].include?(found)
                        puts "#{pkg}: #{found} provides #{require} (weak depedendency ignored) for #{files.collect {|file| file.sub("/usr/lib/ruby/","") }.join(",")}"
		else
			puts "#{pkg}: #{found} provides #{require} for #{files.collect {|file| file.sub("/usr/lib/ruby/","") }.join(",")}"
			package_dependencies[pkg] += [found]
		end
	end
end
if failed
	puts "There is some missing requirements not mapped to files in packages."
	puts "Please, fix the missing files or ignore them on require_ignore var"
	exit(1)
end
# Remove self dependency
package_dependencies = Hash[package_dependencies.collect {|(pkg,deps)| [pkg,package_dependencies[pkg]=deps.uniq.sort - [pkg]]}]
package_dependencies.default = []

puts "Expanding dependencies..."
begin
	changed=false
	package_dependencies.each do
		|(pkg,deps)|
		next if deps.empty?
		deps.each {|dep| puts "#{pkg}: #{dep} also depends on #{pkg}" if package_dependencies[dep].include?(pkg) }
		deps_new = deps.collect {|dep| [dep] + package_dependencies[dep] }.inject([],:+).uniq.sort
		if not deps == deps_new
			puts "#{pkg}: {deps.join(",")} (OLD)"
			puts "#{pkg}: #{deps_new.join(",")} (NEW)"
			package_dependencies[pkg]=deps_new

			if deps_new.include?(pkg)
				$stderr.puts "#{pkg}: Circular dependency detected (#1)!"
				exit 1
			end
			changed=true
		end
	end
end if not changed

puts "Removing redundant dependencies..."
package_dependencies.each do
	|(pkg,deps)|
	package_dependencies[pkg]=deps.uniq - [pkg]
end

puts "Checking for mutual dependencies..."
package_dependencies.each do
	|(pkg,deps)|
	if deps.include? pkg
		$stderr.puts "#{pkg}: Circular dependency detected (#2)!"
		failed = true
	end
end
exit(1) if failed


package_dependencies2=package_dependencies.dup
package_dependencies.each do
	|(pkg,deps)|

	# Ignore dependencies that are already required by another dependency
	deps_clean = deps.reject {|dep_suspect| deps.detect {|dep_provider|
			if package_dependencies[dep_provider].include?(dep_suspect)
				puts "#{pkg}: #{dep_suspect} is already required by #{dep_provider}"
				true
			end
		 } }

	if not deps==deps_clean
		puts "before: #{deps.join(",")}"
		puts "after: #{deps_clean.join(",")}"
		package_dependencies2[pkg]=deps_clean
	end
end
package_dependencies=package_dependencies2

puts "Checking current packages dependencies..."
ok=true
package_dependencies.each do
	|(pkg,deps)|
	current_deps=`opkg depends #{pkg} | sed -r -e '1d;s/^[[:blank:]]*//'`.split("\n")
	current_deps.reject!{|dep| dep =~ /^lib/ }
	current_deps -= ["ruby"]

	extra_dep = current_deps - deps
	$stderr.puts "Package #{pkg} does not need to depend on #{extra_dep.join(" ")} " if not extra_dep.empty?
	missing_dep = deps - current_deps
	$stderr.puts "Package #{pkg} needs to depend on #{missing_dep.join(" ")} " if not missing_dep.empty?

	if not extra_dep.empty? or not missing_dep.empty?
		$stderr.puts "define Package/#{pkg}"
		$stderr.puts "  DEPENDS:=ruby#{([""] +deps).join(" +")}"
		ok=false
	end
end

puts "All dependencies are OK." if ok

__END__
